Skip to content

[7/10] libc, sched: Resolve FDPIC descriptors at module callback entry points - #20130

Open
casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:fdpic-callbacks
Open

casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:fdpic-callbacks

Conversation

@casaroli

@casaroli casaroli commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Summary

[5/10] #19942, [5.5/10] #19940 and [6/10] #20089 are merged: an FDPIC object is placed, bound and relocated. This makes the firmware callable from one.

The base firmware and an FDPIC module disagree about what a function pointer is. Firmware is not built FDPIC, so to it a pointer is a code address and it branches there. A module passes the address of a two word descriptor instead, because its code and data are placed independently and a bare code address would leave the callee unable to find its own data. A firmware routine handed a module's callback therefore branches into the module's data segment and faults.

So the ten entry points that can be handed a callback resolve the descriptor before storing or branching to it: qsort, bsearch, pthread_create, signal, sigaction, task_create and task_create_with_stack, task_spawn, pthread_once, scandir, and mq_notify and timer_create with SIGEV_THREAD.

Which one resolves matters as much as that one does. Resolving twice would take an already resolved code address for a descriptor and read two words from the instruction stream, so each pointer is resolved exactly once, at the outermost point that sees it. signal() passes its argument through untouched because sigaction() and then nxsig_action() will resolve it, which also covers a module calling sigaction() directly. qsort() is split so the public entry resolves and the recursive implementation does not. scandir() resolves its filter but not its comparison function, which it hands to qsort().

Whether the caller is a module at all is asked of the PIC base register, which up_initial_state() sets only for a task that has a D-Space, so a plain kernel task reads zero and is left alone.

SIGEV_THREAD is the case the register cannot answer, because the callback runs later on a work queue worker carrying no module's base. The base is captured when the notification is registered, in the module's own context, and installed around the call.

Impact

All of it is behind CONFIG_FDPIC, which defaults off and is only selectable where ARCH_HAVE_ELF_FDPIC is set.

qsort() gains an internal split and scandir() a blank line; everything else is #ifdef'd. With the option off the entry points are what they were.

Testing

mps3-an547:picostest builds with CONFIG_FDPIC off and on.

tools/checkpatch.sh -c -u -m -g passes.

The run time evidence for the descriptors themselves is in [10/10], whose pimoroni-pico-2-plus:xipfs-fdpic carries apps/testing/fs/xipfs: it exercises qsort and SIGEV_THREAD callbacks from a loaded module, which is what this patch exists to make work.

@github-actions github-actions Bot added Arch: arm Issues related to ARM (32-bit) architecture Area: OS Components OS Components issues Size: L The size of the change in this PR is large labels Sep 13, 2026
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

hifive1-revb

  • flash: .text +4 B (+0.0%, 83,728 B / 4,194,304 B, total: 2% used)

qemu-armv8a

  • Code: .text.qsort -896 B (+0.0%, 344,368 B)

qemu-intel64

  • Code: .text +16 B (+0.0%, 8,659,785 B)

s698pm-dkit

@github-actions github-actions Bot added Size: M The size of the change in this PR is medium and removed Arch: arm Issues related to ARM (32-bit) architecture Size: L The size of the change in this PR is large labels Sep 13, 2026
@github-actions

Copy link
Copy Markdown

🔗 Cross-repo PR dependencies

The read-only Build run reported the following dependent PR(s) and fetched head SHA(s):

CI run: https://github.com/apache/nuttx/actions/runs/34770810263

Comment thread sched/timer/timer_create.c Outdated
Comment thread sched/signal/sig_notification.c Outdated
Comment thread sched/mqueue/mq_notify.c Outdated
@github-actions

Copy link
Copy Markdown

❌ Cross-repo dependency could not be applied

The Build report says the declared dependency PR(s) could not be applied, so CI did not run against the combined code:

Reason: cherry-pick failed (if your PR has merge commits, rebase instead)

CI run: https://github.com/apache/nuttx/actions/runs/34837949470

acassis
acassis previously approved these changes Sep 14, 2026
Comment thread sched/task/task_create.c
* forwarder. The new task inherits the creator's D-Space.
*/

entry = (main_t)fdpic_callback((FAR void *)entry);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but why not change the invocation point by fdpic_invoke instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new task already gets r9 from the inherited dspace (nxtask_dup_dspace() -> up_initial_state() -> REG_PIC) and keeps it for its life, so only the entry needs unwrapping; fdpic_invoke() is for a callback on a shared worker, which has no base.

Comment thread sched/timer/timer_create.c Outdated
@casaroli
casaroli force-pushed the fdpic-callbacks branch 2 times, most recently from 6f09b03 to 3426186 Compare September 15, 2026 07:21
acassis
acassis previously approved these changes Sep 15, 2026
Comment thread include/nuttx/fdpic.h Outdated
# define fdpic_invoke(arg, entry, got) \
((void)(got), (((CODE void (*)(uintptr_t))(uintptr_t)(entry))(arg)))
# define fdpic_invoke(arg, desc) \
(((CODE void (*)(uintptr_t))(uintptr_t)(desc)->entry)(arg))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we cast desc to function pointer directly for no fdpic case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can do it, for no benefit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread include/nuttx/signal.h
* captured at registration and installed
* around the call on the worker thread. */
#else
sigev_notify_function_t func; /* Notification function */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why need define desc at line 74? I suppose that func should point to fdpic_desc_s in fdpic case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the firmware and the built-in applications are not FDPIC, so their sigev_notify_function is plain. Only way to tell them apart is the registering context.

* the current directory entry, do so.
*/

if (filter && !filter(d))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not change to fdpic_invoke(d, filter) here? and revert the change at line 103

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fdpic_invoke() returns void, and scandir() needs the return code

Comment thread libs/libc/signal/sig_signal.c Outdated

DEBUGASSERT(func != SIG_ERR && func != SIG_HOLD);

/* Not resolved here. nxsig_action() resolves the handler, which covers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the comment?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


if (work->desc.got != 0)
{
fdpic_invoke((uintptr_t)work->value.sival_ptr, &work->desc);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not call fdpic_invoke(work->value, xxx->sigev_notify_function) directly? and remove the change in mq_notify.c and desc field in sigwork_s.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get it. nxsig_notification() does not run in the registering context. Drivers, timer_settime() and mq_sndinternal() may not run in the module context.

The base firmware and an FDPIC module disagree about what a function
pointer is.  Firmware is not built FDPIC, so to it a pointer is a code
address and it branches there.  A module passes the address of a two word
descriptor instead, because its code and data are placed independently and
a bare code address would leave the callee unable to find its own data.  A
firmware routine that takes a callback therefore branches into the
module's data segment and faults.

So the ten entry points that can be handed a callback by a module resolve
the descriptor before storing or branching to it: qsort, bsearch,
pthread_create, signal, sigaction, task_create and task_create_with_stack,
task_spawn, pthread_once, scandir, and mq_notify and timer_create with
SIGEV_THREAD.

Which one resolves matters as much as that one does.  Resolving twice would
take an already resolved code address for a descriptor and read two words
from the instruction stream, so each pointer is resolved exactly once, at
the outermost point that sees it.  signal() passes its argument through
untouched because sigaction() and then nxsig_action() will resolve it,
which covers a module calling sigaction() directly as well.  qsort() is
split so that the public entry resolves and the recursive implementation
does not.  scandir() resolves its filter but not its comparison function,
which it hands to qsort().

Whether a caller is a module at all is asked of the PIC base register,
which up_initial_state() sets only for a task that has a D-Space.  A plain
kernel task therefore reads zero and is left alone.

SIGEV_THREAD is the case the register cannot answer, because the callback
runs later on a work queue worker that carries no module's base at all.
The base is captured instead when the notification is registered, in the
module's own context, and installed around the call.

All of it is behind CONFIG_FDPIC, which defaults off.  Built for
mps3-an547:picostest both ways; with it off the entry points compile to
what they were.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
nxstyle wants a blank line between a declaration and the statements that
follow it.  The line is not new, but it sits within three lines of the
FDPIC change in this series, so CI reads it as part of the patch.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: OS Components OS Components issues Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants